<?php
/**
 * Набор базовых классов библиотеки W3xmlrpcClient
 *
 * @author Evgeny Blinov <e.a.blinov@gmail.com>
 * @license New BSD License
 * @link http://code.google.com/p/w3lib/
 * @package W3xmlrpcClient
 * @version $Id: W3xmlrpcClient.php 4 2011-10-17 23:48:11Z E.A.Blinov@gmail.com $
 */

interface _W3xmlrpcAdapter{
    public function request($url, $request);
}

class W3xmlrpcClientException extends Exception{}
class W3xmlrpcCallException extends W3xmlrpcClientException{}

class W3Base64Data {
    protected $data;

    public function __construct($data) {
        $this->data = $data;
    }

    public function getEncoded(){
        return base64_encode($this->data);
    }
}

class W3xmlrpcClient {
    const UA = 'W3xmlrpcClient 0.1';

    public $url;
    public $prefix;
    public $useDateTimeObject = false;
    public $useExtended       = true;
    protected $Adapter;

    protected function decodeVar($Element){
        if (!($Element instanceof SimpleXMLElement && $Element->count()))
            throw new W3xmlrpcClientException('Invalid markup. Not found node');

        list($Node) = $Element->children();
        switch (strtolower($Node->getName())) {
            case 'nil':
                return null;
            case 'i4':
            case 'int':
                return intval(strval($Node));
            case 'boolean':
                return intval(strval($Node))?true:false;
            case 'datetime.iso8601':
                if ($this->useDateTimeObject)
                    return new DateTime(strval($Node));
            case 'string':
                return strval($Node);
            case 'double':
                return floatval(strval($Node));
            case 'base64':
                return base64_decode(strval($Node));
            case 'array':
                $return = array();
                foreach($Node->data->value AS $Value)
                    $return[] = $this->decodeVar($Value);
                return $return;
            case 'struct':
                $return = array();
                foreach($Node->member AS $Member)
                    $return[strval($Member->name)] = $this->decodeVar($Member->value);
                return $return;
            default :
                throw new W3xmlrpcClientException('Invalid markup. Unexpected node: '.$Node->getName());
        }
    }
    protected function encodeVar(&$var, $indent = 0){
        switch (gettype($var)){
            case 'boolean':
                return '<boolean>'.($var?1:0).'</boolean>';
            case 'integer':
                return '<int>'.$var.'</int>';
            case 'double':
                return '<double>'.$var.'</double>';
            case 'NULL':
                if ($this->useExtended)
                    return '<nil/>';
            case 'string':
                return '<string>'.htmlspecialchars($var).'</string>';
            case 'array':
                $i = 0;
                $return = '<array><data>';
                    foreach ($var AS $key => $value){
                        if ($key !== $i++){
                            $return = false;
                            break;
                        }
                        $return .= '<value>'.$this->encodeVar($value).'</value>';
                    }
                if ($return){
                    $return .= '</data></array>';
                    return $return;
                }
            case 'object':
                if ($var instanceof W3Base64Data)
                    return '<base64>'.$var->getEncoded().'</base64>' ;
                elseif ($var instanceof DateTime)
                    return '<datetime.iso8601>'.$var->format(DateTime::ISO8601).'</datetime.iso8601>' ;
                else {
                    $return = '<struct>';
                    foreach ($var AS $key => $value){
                        $return .= '<member>';
                            $return .= '<name>'.htmlspecialchars($key).'</name>';
                            $return .= '<value>'.$this->encodeVar($value).'</value>';
                        $return .= '</member>';
                    }
                    $return .= '</struct>\n';
                    return $return;
                }
            default :
                throw new W3xmlrpcClientException('Bad parameter. Unsupported type: '.gettype($var));
        }

    }
    protected function parseResponse($string){
        $Response = simplexml_load_string($string);
        if ($Response === false)
            throw new W3xmlrpcClientException('Invalid response. '.libxml_get_last_error()->message);

        list($Node) = $Response->children();
        switch (strtolower($Node->getName())){
            case 'params':
                return $this->decodeVar($Node->param->value);
            case 'fault':
                $fault = $this->decodeVar($Node->value);
                throw new W3xmlrpcCallException($fault['faultString'], $fault['faultCode']);
            default :
                throw new W3xmlrpcClientException('Invalid markup. Unexpected node: '.$Node->getName());
        }
    }
    protected function formRequest($methodName, array $params = array()){
        $request  = '<?xml version="1.0"?>'."\n";
        $request .= '<methodCall>';
        $request .= '<methodName>'.htmlspecialchars($methodName).'</methodName>';
        $request .= '<params>';

        foreach($params AS $param)
            $request .= '<param><value>'.$this->encodeVar($param).'</value></param>';

        $request .= '</params></methodCall>';
        return $request;
    }

    public function getAdapter(){
        return $this->Adapter;
    }

    public function __construct($uri, $prefix = '', _W3xmlrpcAdapter $Adapter = null) {
        $this->prefix  = $prefix;
        $this->url     = $uri;

        if ($Adapter === null)
            $this->Adapter = new W3xmlrpcAdapterInternal;
        else
            $this->Adapter = $Adapter;
    }
    public function __call($name, $parameters) {
        $request = $this->formRequest($this->prefix.$name, $parameters);
        if (!$response = $this->getAdapter()->request($this->url, $request))
            throw new W3xmlrpcClientException('Empty response');

        return $this->parseResponse($response);
    }
}